package lumis.portal.service;

import lumis.portal.AccessDeniedException;
import lumis.portal.PortalContext;
import lumis.portal.PortalException;
import lumis.portal.PortalObjectNotFoundException;
import lumis.portal.UnexpectedException;
import lumis.portal.acl.PortalPermissions;
import lumis.portal.authentication.SessionConfig;
import lumis.portal.cache.PortalCache;
import lumis.portal.clock.ClockConfig;
import lumis.portal.clock.IClockManager;
import lumis.portal.dao.DaoFactory;
import lumis.portal.manager.ManagerException;
import lumis.portal.manager.ManagerFactory;
import lumis.portal.service.acl.ServicePermissions;
import lumis.portal.service.type.IServiceTypeDao;
import lumis.portal.service.type.ServiceTypeConfig;
import lumis.portal.serviceinterface.IServiceInterfaceManager;
import lumis.portal.serviceinterface.ServiceInterfaceConfig;
import lumis.portal.util.PortalUtilInternal;
import lumis.util.ITransaction;
import lumis.util.XmlUtil;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.w3c.dom.Document;
import org.w3c.dom.Node;

/**
 * Manager for all operations on Services
 * 
 * @version $Revision: 12538 $ $Date: 2011-02-09 12:07:10 -0200 (Wed, 09 Feb 2011) $
 * @since 4.0.0
 */
public class ServiceManager implements IServiceManager
{
	// service types and service interface types configurations
	private PortalCache<ServiceTypeConfig> serviceTypeConfigs = new PortalCache<ServiceTypeConfig>("lumis.portal.service.type.ServiceTypeConfig");

	// service and service interface class instances
	private PortalCache<IService> services = new PortalCache<IService>("lumis.portal.service.IService");

	/**
	 * Service
	 */
	public void register(SessionConfig sessionConfig, String serviceConfigPath, ITransaction transaction) throws PortalException
	{
		if (!ManagerFactory.getPortalAclManager().checkPermission(sessionConfig, PortalPermissions.MANAGE_SERVICE, transaction))
			throw new AccessDeniedException();

		Document serviceConfig = PortalUtilInternal.getDefinitionDocument(serviceConfigPath + "/servicedefinition.xml");

		// TODO: validate the service configuration

		Node serviceXmlNode = XmlUtil.selectSingleNode("serviceDefinition/service", serviceConfig);
		if (serviceXmlNode != null)
			addOrUpdateService(sessionConfig, serviceXmlNode, serviceConfigPath, transaction);
	}

	public void unregister(SessionConfig sessionConfig, String serviceId, ITransaction transaction) throws ManagerException, PortalException
	{
		try
		{
			if (!ManagerFactory.getPortalAclManager().checkPermission(sessionConfig, PortalPermissions.MANAGE_SERVICE, transaction))
				throw new AccessDeniedException();

			ServiceConfig serviceConfig = get(sessionConfig, serviceId, transaction);
			if (serviceConfig != null)
			{
				// check if there exists a service instance for the service
				Collection<String> serviceInstanceIds = ManagerFactory.getServiceInstanceManager().getIdsByServiceId(sessionConfig, serviceId, transaction);
				if (!serviceInstanceIds.isEmpty())
					throw new ManagerException("STR_SERVICE_IS_INSTANTIATED;" + serviceId);
				
				IService service = ManagerFactory.getServiceManager().getService(sessionConfig, serviceConfig.getId(), transaction);
				service.preUnregister(sessionConfig, transaction);

				// unregister each service interface
				IServiceInterfaceManager serviceInterfaceManager = ManagerFactory.getServiceInterfaceManager();
				Collection<String> serviceInterfaceIds = serviceInterfaceManager.getIdsByServiceId(serviceId, transaction);				
				for (String serviceInterfaceId: serviceInterfaceIds)
				{
					ServiceInterfaceConfig serviceInterfaceConfig = serviceInterfaceManager.getServiceInterfaceConfig(serviceInterfaceId, transaction);
					serviceInterfaceManager.unregister(sessionConfig, serviceInterfaceConfig, transaction);
				}
				
				// unregister the service's clock
				IClockManager clockManager = ManagerFactory.getClockManager();
				Collection<String> existentClocks = clockManager.getClockIdsByServiceId(sessionConfig, serviceConfig.getId(), transaction);
				for (String clock : existentClocks)
					clockManager.delete(sessionConfig, clock, transaction);

				service.postUnregister(sessionConfig, transaction);

				// remove the service and clear the cache
				DaoFactory.getServiceDao().delete(serviceId, transaction);
				services.remove(serviceConfig.getId(), transaction);
				
				service.unload(serviceConfig);
				
			}
		}
		catch (PortalException e)
		{
			throw e;
		}
		catch (Exception err)
		{
			throw new UnexpectedException(err);
		}
	}

	private void addOrUpdateService(SessionConfig sessionConfig, Node serviceXmlNode, String baseUrl, ITransaction transaction) throws ManagerException, PortalException
	{
		if (!ManagerFactory.getPortalAclManager().checkPermission(sessionConfig, PortalPermissions.MANAGE_SERVICE, transaction))
			throw new AccessDeniedException();
		try
		{
			
			try
			{
				ServiceConfig serviceConfig = DaoFactory.getServiceDao().getByBaseUrl(baseUrl, transaction);
				String serviceId = XmlUtil.readAttributeString("id", serviceXmlNode);
				if(!serviceConfig.getId().equals(serviceId))
					throw new ManagerException("STR_CANNOT_CHANGE_SERVICE_ID;"+serviceConfig.getId()+";"+serviceId);
			}
			catch(PortalObjectNotFoundException e)
			{
				;
			}
			
			// add the baseUrl Node
			XmlUtil.addTextNode(serviceXmlNode, "baseUrl", baseUrl);

			// instantiate the implementation class and allow it to register the service
			String implementationClass = XmlUtil.readAttributeString("className", serviceXmlNode);
			String serviceType = XmlUtil.readAttributeString("type", serviceXmlNode, "lum_service");
			if(implementationClass == null && serviceType != null)
			{
				ServiceTypeConfig serviceTypeConfig = getServiceTypeConfig(sessionConfig, serviceType, transaction);
				
				if(serviceTypeConfig.getClassName() != null)
					implementationClass = serviceTypeConfig.getClassName();
				else
					implementationClass = "lumis.portal.service.GenericService";
			}
			
			IService serviceImplementation = (IService)ManagerFactory.getDeploymentManager().getClassLoader().loadClass(implementationClass).newInstance();
			serviceImplementation.preRegister(sessionConfig, serviceXmlNode, transaction);

			// save the service definition
			ServiceConfig serviceConfig = new ServiceConfig();
			serviceConfig.setDefinition(serviceXmlNode);
			
			try
			{
				ServiceConfig serviceConfigLast = get(sessionConfig, serviceConfig.getId(), transaction);
				
				if(serviceConfig.getAccessControlListId() == null)
					serviceConfig.setAccessControlListId(serviceConfigLast.getAccessControlListId());
				
				// if service already exists, update it
				DaoFactory.getServiceDao().update(serviceConfig, transaction);
			}
			catch (PortalObjectNotFoundException e)
			{
				// if service doesn't exist, add it
				ManagerFactory.getServiceAclManager().add(sessionConfig, serviceConfig, transaction);
				DaoFactory.getServiceDao().add(serviceConfig, transaction);
			}
			
			serviceConfig = get(sessionConfig, serviceConfig.getId(), transaction);

			// parse service clocks removing depracated clocks
			IClockManager clockManager = ManagerFactory.getClockManager();
			Collection<String> existentClocks = clockManager.getClockIdsByServiceId(sessionConfig, serviceConfig.getId(), transaction);
			Set<String> excludeClocksSet = new HashSet<String>(existentClocks);
			
			Node serviceClocks = XmlUtil.selectSingleNode("clocks", serviceXmlNode);
			if(serviceClocks == null)
			{
				IService service = ManagerFactory.getServiceManager().getService(sessionConfig, serviceConfig.getId(), transaction);
				if(service instanceof IServiceClockDefinitionProvider)
					serviceClocks = ((IServiceClockDefinitionProvider)service).getClocks(sessionConfig, serviceConfig, transaction);
			}
			
			if(serviceClocks != null)
			{
				Node[] clocks = XmlUtil.selectNodes("clock", serviceClocks);
				ClockConfig[] clockConfigs = new ClockConfig[clocks.length];
				for(int i = 0; i < clockConfigs.length; i++)
				{
					String clockId = XmlUtil.readAttributeString("id", clocks[i]);
					clockConfigs[i] = clockManager.get(sessionConfig, serviceConfig.getId() +  "." +  clockId, transaction);
					
					if(clockConfigs[i] == null)
						clockConfigs[i] = new ClockConfig();
					
					clockConfigs[i].setDefinition(clocks[i]);
					clockManager.addOrUpdate(sessionConfig, clockConfigs[i], transaction);
					excludeClocksSet.remove(clockConfigs[i].getId());
				}
			}
			
			for (String clock : excludeClocksSet)
				clockManager.delete(sessionConfig, clock, transaction);

			ManagerFactory.getServiceInterfaceManager().register(sessionConfig, baseUrl, transaction);

			// clear service's ACLs, since permissions definition may have changed
			for(String serviceInstanceId: ManagerFactory.getServiceInstanceManager().getIdsByServiceId(sessionConfig, serviceConfig.getId(), transaction))
				ManagerFactory.getServiceInstanceAclManager().clearCache(sessionConfig, serviceInstanceId, transaction);
			
			serviceImplementation.postRegister(sessionConfig, serviceConfig, transaction);
			
			// clear the service instance acls cache
			Collection<String> serviceInstanceIds = ManagerFactory.getServiceInstanceManager().getIdsByServiceId(sessionConfig, serviceConfig.getId(), transaction);
			for(String serviceInstanceId : serviceInstanceIds)
				ManagerFactory.getServiceInstanceAclManager().clearCache(sessionConfig, serviceInstanceId, transaction);
			
			// clear service cache
			services.remove(serviceConfig.getId(), transaction);
		}
		catch (PortalException err)
		{
			throw err;
		}
		catch (Exception err)
		{
			throw new UnexpectedException(err);
		}
	}

	public ServiceConfig get(SessionConfig sessionConfig, String serviceId, ITransaction transaction) throws ManagerException, PortalException
	{
		ServiceConfig serviceConfig = DaoFactory.getServiceDao().get(serviceId, transaction);
		if(serviceConfig.getImplementationClass() == null && serviceConfig.getServiceType() != null)
		{
			ServiceTypeConfig serviceTypeConfig = getServiceTypeConfig(sessionConfig, serviceConfig.getServiceType(), transaction);
			
			if(serviceTypeConfig.getClassName() != null)
				serviceConfig.setImplementationClass(serviceTypeConfig.getClassName());
			else
				serviceConfig.setImplementationClass("lumis.portal.service.GenericService");
		}
		return serviceConfig;
	}

	public synchronized IService getService(SessionConfig sessionConfig, String serviceId, ITransaction transaction) throws ManagerException, PortalException
	{
		IService service = services.get(serviceId);

		if (service == null)
		{
			ServiceConfig serviceConfig = get(sessionConfig, serviceId, transaction);

			String implementationClass = serviceConfig.getImplementationClass();

			if (implementationClass == null)
			{
				ServiceTypeConfig serviceType = getServiceTypeConfig(sessionConfig, serviceConfig.getServiceType(), transaction);
				implementationClass = serviceType.getClassName();
			}
			
			try
			{
				service = (IService)ManagerFactory.getDeploymentManager().getClassLoader().loadClass(implementationClass).newInstance();
				service.load(serviceConfig);
				services.put(serviceId, service);
			}
			catch(Exception e)
			{
				throw new UnexpectedException(e);
			}
		}

		return service;
	}
	
	public Collection<ServiceConfig> getServices(SessionConfig sessionConfig, boolean getInstantiableServicesOnly, ITransaction transaction) throws ManagerException, PortalException
	{
		Collection<String> serviceIds = DaoFactory.getServiceDao().getServicesIds(getInstantiableServicesOnly, transaction);
		Collection<ServiceConfig> serviceConfigs = new ArrayList<ServiceConfig>(serviceIds.size());
		
		for(String serviceId: serviceIds)
		{
			ServiceConfig serviceConfig = get(sessionConfig, serviceId, transaction);
			if(ManagerFactory.getServiceAclManager().checkPermission(sessionConfig,serviceConfig.getId(), ServicePermissions.CREATE_SERVICE_INSTANCE,transaction))
				serviceConfigs.add(serviceConfig);
		}
		
		return serviceConfigs;
	}

	public void registerServiceTypes(SessionConfig sessionConfig, String serviceTypesConfigPath, ITransaction transaction) throws ManagerException, PortalException
	{
		if (!ManagerFactory.getPortalAclManager().checkPermission(sessionConfig, PortalPermissions.MANAGE_PORTAL, transaction))
			throw new AccessDeniedException();

		Document controlDef = PortalUtilInternal.getDefinitionDocument(serviceTypesConfigPath + "/servicetypesdefinition.xml");

		//ServiceInterfaceTypeConfig serviceInterfaceTypeConfig;

		// TODO: validate the content configuration: type must always be
		// specified

		// service types

		Node[] serviceTypeNodes = XmlUtil.selectNodes("serviceTypesDefinition/services/service", controlDef);
		Node typeNode = null;

		for (int i = 0; i < serviceTypeNodes.length; i++)
		{
			typeNode = serviceTypeNodes[i];
			ServiceTypeConfig serviceTypeConfig = new ServiceTypeConfig();

			String type = XmlUtil.readNodeString("type", typeNode);
			String className = XmlUtil.readNodeString("className", typeNode, "");

			if (className.trim().length() == 0)
				className = null;

			serviceTypeConfig.setClassName(className);
			serviceTypeConfig.setType(type);

			DaoFactory.getServiceTypeDao().addOrUpdate(serviceTypeConfig, transaction);
			serviceTypeConfigs.put(serviceTypeConfig.getId(), serviceTypeConfig);
		}

		// service interface types
		ManagerFactory.getServiceInterfaceManager().registerServiceInterfaceTypes(sessionConfig, serviceTypesConfigPath, transaction);
	}

	public void unregisterServiceType(SessionConfig sessionConfig, String typeId, ITransaction transaction) throws ManagerException, PortalException
	{	
		if (!ManagerFactory.getPortalAclManager().checkPermission(sessionConfig, PortalPermissions.MANAGE_PORTAL, transaction))
			throw new AccessDeniedException();
		IServiceTypeDao serviceTypeDao = DaoFactory.getServiceTypeDao();
		serviceTypeDao.delete(typeId, transaction);
		serviceTypeConfigs.remove(typeId, transaction);
	}

	public synchronized ServiceTypeConfig getServiceTypeConfig(SessionConfig sessionConfig, String serviceType, ITransaction transaction) throws ManagerException, PortalException
	{
		ServiceTypeConfig serviceTypeConfig = serviceTypeConfigs.get(serviceType.toLowerCase());
		
		if (serviceTypeConfig == null)
		{
			serviceTypeConfig = DaoFactory.getServiceTypeDao().get(serviceType, transaction);
			serviceTypeConfigs.put(serviceTypeConfig.getId(), serviceTypeConfig);
		}

		return serviceTypeConfig;
	}

	public Collection<String> getPreBuiltChannelFiles(SessionConfig sessionConfig, String serviceId, ITransaction transaction) throws PortalException
	{
		// get the service pre built channels path
		ServiceConfig serviceConfig = get(sessionConfig, serviceId, transaction);
		String servicePreBuiltChannelsRelativePath = serviceConfig.getBaseUrl() + "/prebuiltchannels";
		String servicePreBuiltChannelsPath = PortalContext.getDefinitionPath(servicePreBuiltChannelsRelativePath);
		
		// list the files in the pre built channels folder
		File servicePreBuiltChannelsFolder = new File(servicePreBuiltChannelsPath);
		String[] files = servicePreBuiltChannelsFolder.list();
		if (files == null)
			return Collections.emptyList();

		// include all files with .lec extension in the result
		Collection<String> result = new ArrayList<String>(files.length);
		for (String file: files)
		{
			if (file.endsWith(".lec"))
				result.add(servicePreBuiltChannelsRelativePath + "/" + file);
		}
		
		return result;
	}
	
	public void clearCache() throws PortalException
	{
		DaoFactory.getServiceDao().clearCache();
		services.clear();
		serviceTypeConfigs.clear();
	}

	public boolean keyExists(SessionConfig sessionConfig, Object configKey, Object config, ITransaction transaction)
	{
		ServiceConfig serviceConfig = null;
		try
		{
			serviceConfig = get(sessionConfig, (String)configKey, transaction);
		}
		catch (PortalObjectNotFoundException e)
		{
		}
		catch (PortalException e)
		{
			throw new IllegalStateException("Could not check a service key persistence.");
		}
		
		return serviceConfig != null;
	}
}
